package org.codefilarete.tool;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

import org.codefilarete.tool.function.Functions;
import org.codefilarete.tool.function.ThrowingConsumer;
import org.codefilarete.tool.function.ThrowingFunction;

/**
 * Class that tries to fullfill use cases on which {@link java.util.Optional} is insufficient
 *
 * @author Guillaume Mary
 */
public class Nullable<T> implements Supplier<T> {
	
	/**
	 * Shortcut for new Nullable(value)
	 * 
	 * @param value a nullable value
	 * @param <T> value type
	 * @return new Nullable(value)
	 */
	public static <T> Nullable<T> nullable(@javax.annotation.Nullable T value) {
		return new Nullable<>(value);
	}
	
	/**
	 * Shortcut for new Nullable(value)
	 *
	 * @param value a nullable value
	 * @param <T> value type
	 * @return new Nullable(value)
	 */
	public static <T> Nullable<T> nullable(Supplier<T> value) {
		return new Nullable<>(value);
	}
	
	/**
	 * Shortcut for new Nullable(input) + {@link #map(Function)}
	 *
	 * @param input a nullable value
	 * @param f a function to be applied on the input
	 * @param <I> value type
	 * @return new Nullable(input).map(f)
	 */
	public static <I, O> Nullable<O> nullable(@javax.annotation.Nullable I input, Function<I, O> f) {
		return nullable(input).map(f);
	}
	
	/**
	 * Shortcut for new Nullable(input) + {@link #map(Function)}
	 *
	 * @param input a nullable value
	 * @param f a function to be applied on the input
	 * @param <I> value type
	 * @return new Nullable(input).map(f)
	 */
	public static <I, O> Nullable<O> nullable(Supplier<I> input, Function<I, O> f) {
		return nullable(input).map(f);
	}
	
	/**
	 * Shortcut for new Nullable(input) + {@link #map(Function)}
	 *
	 * @param input a nullable value
	 * @param f1 a function to be applied on the input
	 * @param f2 a second function to be applied on the result of the first function
	 * @param <I> value type
	 * @return new Nullable(input).map(f1).map(f2)
	 */
	public static <I, O, A> Nullable<O> nullable(@javax.annotation.Nullable I input, Function<I, A> f1, Function<A, O> f2) {
		return nullable(input).map(f1).map(f2);
	}
	
	/**
	 * Shortcut for new Nullable(input) + {@link #map(Function)}
	 *
	 * @param input a nullable value
	 * @param f1 a function to be applied on the input
	 * @param f2 a second function to be applied on the result of the first function
	 * @param <I> value type
	 * @return new Nullable(input).map(f1).map(f2)
	 */
	public static <I, O, A> Nullable<O> nullable(Supplier<I> input, Function<I, A> f1, Function<A, O> f2) {
		return nullable(input).map(f1).map(f2);
	}
	
	/**
	 * @param <T> type of the {@link Nullable}
	 * @return a {@link Nullable} of null
	 */
	public static <T> Nullable<T> empty() {
		return Nullable.nullable(() -> null);
	}
	
	/** The payload, may be null itself or owned value may be null */
	private Supplier<T> value;
	
	/**
	 * Constructor from a nullable value
	 * @param value a nullable object
	 */
	private Nullable(@javax.annotation.Nullable T value) {
		this.value = () -> value;
	}
	
	/**
	 * Constructor from a non null supplier which may supply a null value
	 * @param value a non null supplier
	 */
	private Nullable(Supplier<T> value) {
		this.value = value;
	}
	
	/**
	 * @return true if internal value is not null
	 */
	public boolean isPresent() {
		return value.get() != null;
	}
	
	/**
	 * @return true if internal value is null
	 */
	public boolean isAbsent() {
		return value.get() == null;
	}
	
	/**
	 * Return current value: null or not
	 * 
	 * @return internal value
	 */
	@Override
	@javax.annotation.Nullable
	public T get() {
		return value == null ? null : value.get();
	}
	
	/**
	 * Replaces value by given one
	 * 
	 * @param value a new value, may be null
	 * @return this
	 */
	public Nullable<T> set(@javax.annotation.Nullable T value) {
		this.value = () -> value;
		return this;
	}
	
	public Nullable<T> setIfAbsent(@javax.annotation.Nullable T value) {
		if (isAbsent()) {
			this.value = () -> value;
		}
		return this;
	}
	
	/**
	 * Gives another value if current is null
	 * 
	 * @param anotherValue the other value to return in case of current one is null
	 * @return {@link #get} or anotherValue if non present value
	 */
	@javax.annotation.Nullable
	public T getOr(@javax.annotation.Nullable T anotherValue) {
		return getOr(() -> anotherValue);
	}
	
	/**
	 * Gives another value if current is null
	 *
	 * @param anotherValue the supplier called in case of current null value
	 * @return {@link #get} or anotherValue.get() if non present value
	 */
	public T getOr(Supplier<T> anotherValue) {
		return !isPresent() ? anotherValue.get() : get();
	}
	
	/**
	 * Gives the current value or throws the given {@link Throwable} in case of missing value.
	 * Shortcut for {@link #elseThrow(Throwable)}.get()
	 *
	 * @param throwable the provider of {@link Throwable} to be thrown
	 * @param <E> the {@link Throwable} type
	 * @return this (if value is present)
	 * @throws E the given throwable
	 */
	public <E extends Throwable> T getOrThrow(Supplier<E> throwable) throws E {
		return elseThrow(throwable).get();
	}
	
	/**
	 * Changes internal value by another if current is null
	 *
	 * @param otherValue the replacing value
	 * @return this
	 */
	public Nullable<T> elseSet(@javax.annotation.Nullable T otherValue) {
		return elseSet(() -> otherValue);
	}
	
	/**
	 * Changes internal value by another if current is null
	 * 
	 * @param otherValue a replacing {@link java.util.function.Supplier}
	 * @return this
	 */
	public Nullable<T> elseSet(Supplier<T> otherValue) {
		if (!isPresent()) {
			value = otherValue;
		}
		return this;
	}
	
	/**
	 * Will throw the given {@link Throwable} if current value is null
	 *
	 * @param throwable the {@link Throwable} to be thrown
	 * @param <E> the {@link Throwable} type
	 * @return this (if value is present)
	 * @throws E the given throwable
	 */
	public <E extends Throwable> Nullable<T> elseThrow(E throwable) throws E {
		return this.<E>elseThrow(() -> throwable);
	}
	
	/**
	 * Will throw supplied {@link Throwable} if current value is null.
	 * Tips to avoid invocation conflict with {@link #elseThrow(Throwable)} : one should specify generics before method call sush as:
	 * <code>
	 *     Nullable.empty().&lt;IOException&gt;elseThrow(IOException::new);
	 * </code>
	 *
	 * @param throwableSupplier the {@link Supplier} that will give the {@link Throwable} to be thrown
	 * @param <E> the {@link Throwable} type
	 * @return this (if value is present)
	 * @throws E the given throwable
	 */
	public <E extends Throwable> Nullable<T> elseThrow(Supplier<E> throwableSupplier) throws E {
		// NB we don't use ifPresent because it's quite too much for this case (and requires to return null in given function, ugly)
		if (!isPresent()) {
			throw throwableSupplier.get();
		} else {
			return this;
		}
	}
	
	/**
	 * Applies a function on current value if present and changes the content of this instance by the new value.
	 *
	 * @param mapper a function to be applied on value
	 * @param <O> the function returned type
	 * @return this instance cast with mapper value type
	 */
	public <O> Nullable<O> map(Function<? super T, ? extends O> mapper) {
		set((T) ifPresent(mapper::apply));
		return (Nullable<O>) this;
	}
	
	/**
	 * Same as {@link #map(Function)} with a throw clause
	 *
	 * @param function a function to be applied on value
	 * @param <O> the function returned type
	 * @param <E> le type d'exception
	 * @return a {@link Nullable} of the function result or of null if value wasn't present
	 * @throws E type of exception thrown by the function
	 */
	public <O, E extends Exception> Nullable<O> mapThrower(ThrowingFunction<? super T, ? extends O, E> function) throws E {
		return nullable(ifPresent(function));
	}
	
	/**
	 * Consumes the value if present
	 *
	 * @param consumer a value consumer
	 * @return this
	 */
	public Nullable<T> invoke(Consumer<T> consumer) {
		return ifPresent(consumer::accept);
	}
	
	/**
	 * Same as {@link #invoke(Consumer)} with a throw clause
	 *
	 * @param consumer a function to be applied on value
	 * @param <E> le type d'exception
	 * @return this
	 * @throws E type of exception thrown by the function
	 */
	public <E extends Exception> Nullable<T> invokeThrower(ThrowingConsumer<? super T, E> consumer) throws E {
		return ifPresent(consumer);
	}
	
	/**
	 * Tests the value if present, if not returns an empty {@link Nullable}
	 * 
	 * @param predicate a predicate on value type
	 * @return this if present and predicate matches, else a {@link Nullable} of null
	 */
	public Nullable<T> filter(Predicate<? super T> predicate) {
		if (isPresent() && predicate.test(get())) {
			return this;
		} else {
			return empty();
		}
	}
	
	/**
	 * Tests the value if present and returns the result
	 * 
	 * @param predicate a predicate on value type, invoked only if value is present
	 * @return {@link Predicate} result wrapped into a {@link Nullable}
	 */
	public Nullable<Boolean> test(Predicate<? super T> predicate) {
		return Nullable.nullable(ifPresent(Functions.toFunction(predicate)::apply));
	}
	
	/** Applies the function if value is present */
	private <O, E extends Exception> O ifPresent(ThrowingFunction<? super T, ? extends O, E> mapper) throws E {
		return isPresent() ? mapper.apply(get()) : null;
	}
	
	/** Applies the consumer if value is present */
	private <E extends Exception> Nullable<T> ifPresent(ThrowingConsumer<? super T, E> consumer) throws E {
		if (isPresent()) {
			consumer.accept(get());
		}
		return this;
	}
}